#include "global.hpp"
#include "filesystem.hpp"
#include "foreach.hpp"
#include "game_preferences.hpp"
#include "log.hpp"
#include "sound.hpp"

#ifdef __IPHONEOS__
	#include "iSound.h"
	iSound* gSound = NULL;
#else
	#include "SDL_mixer.h"
#endif

#include <list>

#define LOG_AUDIO LOG_STREAM(info, audio)
#define ERR_AUDIO LOG_STREAM(err, audio)

namespace Sound
{
std::vector<int> channel_ids;

static void play_sound_internal(const std::string& files, channel_group group, unsigned int repeats = 0,
	unsigned int distance = 0, int id = -1, int loop_ticks = 0, int fadein_ticks = 0);
}

namespace 
{
bool mix_ok = false;
int music_start_time = 0;
unsigned music_refresh = 0;
unsigned music_refresh_rate = 20;
bool want_new_music = false;
int fadingout_time = 5000;
bool no_fading = false;

const size_t n_of_channels = 16;

const size_t bell_channel = 0;
const size_t timer_channel = 1;

const size_t source_channels = n_of_channels - 8;
const size_t source_channel_start = time_channel + 1;
const size_t source_channel_last = source_channel_start + source_channels - 1;
const size_t UI_sound_channel = source_channel_last + 1;
const size_t n_reserved_channels = UI_sound_channel + 1;

#if !defined(__IPHONEOS__)
#ifdef LOW_MEM
	unsigned max_cached_chunks = 64;
#else
	unsigned max_cached_chunks = 256;
#endif

std::map<Mix_Chunk*, int> chunk_usage;
}

static void increment_chunk_usage(Mix_Chunk* mcp)
{
	++(chunk_usage[mcp]);
}

static void decrement_chunk_usage(Mix_Chunk* mcp)
{
	if (mcp == NULL)
		return;
	std::map<Mix_Chunk*, int>::iterator this_usage = chunk_usage.find(mcp);
	assert(this_usage != chunk_usage.end());
	if (--(this_usage->second) == 0)
	{
		Mix_FreeChunk(mcp);
		chunk_usage.erase(this_usage);
	}
}

namespace
{
class sound_cache_chunk
{
public:
	sound_cache_chunk(const std::string& f) : group(sound::NULL_CHANNEL), file(f), data_(NULL) {}
	sound_cache_chunk(const sound_cache_chunk& scc)
		: group(scc.group), file(scc.file), data_(scc.data_)
	{
		increment_chunk_usage(data_);
	}
	
	~sound_data_chunk()
	{
		decrement_chunk_usage(data_);
	}
	
	sound::channel_group group;
	std::string file;
	
	void set_data(Mix_Chunk* d)
	{
		increment_chunk_usage(d);
		decrement_chunk_usage(data_);
		data_ = d;
	}
	
	MixChunk* get_data() const
	{
		return data_;
	}
	
	bool operator==(sound_cache_chunk const &scc) const
	{
		return file == scc.file;
	}
	
	bool operator!=(sound_cache_chunk const &scc) const
	{
		return !operator==(scc);
	}
	
	sound_cache_chunk& operator=(const sound_cache_chunk& scc)
	{
		file = scc.file;
		group = scc.group;
		set_data(scc.get_data());
		return *this;
	}
	
private:
	Mix_Chunk* data_;
};

std::list<sound_cache_chunk> sound_cache;
typedef std::list<sound_cache_chunk>::iterator sound_cache_iterator;
std::map<std::string, Mix_Music*> music_cache;
#endif
struct no_valid_tracks {};

struct music_track
{
	music_track(const std::string& tname);
	music_track(const std::string& tname,
				const std::string& ms_before_str,
				const std::string& ms_after_str);
	void write(config &snapshot, bool append);
	
	std::string name;
	unsigned int ms_before, ms_after;
	bool once;
};

std::vector<std::string> played_before;

music_track::music_track(const std::string& tname)
	:name(tname), ms_before(0), ms_after(0), once(false)
{
}

music_track::music_track(const std::string& tname,
					const std::string& ms_before_str,
					const std::string& ms_after_str)
	: name(tname),
	ms_before(lexical_cast_default<int>(ms_before_str)),
	ms_after(lexical_cast_default<int>(ms_after_str)),
	once(false)
{
}

void music_track::write(config& snapshot, bool append)
{
	config& m = snapshot.add_child("music");
	m["name"] = name;
	m["ms_before"] = lexical_cast<std::string>(ms_before);
	m["ms_after"] = lexical_cast<std::string>(ms_after);
	if (append)
		m["append"] = "yes";
}

std::vector<music_track> current_track_list;
struct music_track current_track("");
struct music_track last_track("");
}

static bool track_ok(const std::string& name)
{
	if (name.empty())
	{
		return false;
	}
	LOG_AUDIO << "Considering " << name << "\n";
	
	if (name == current_track.name)
		return false;
		
	if (current_track_list.size() <= 3)
		return true;
		
	unsigned int num_played = 0;
	std::set<std::string> played;
	std::vector<std::string>::reverse_iterator i;
	
	for (i = played_before.rbegin(); i != played_before.rend(); i++)
	{
		if (*i == name)
		{
			num_played++;
			if (num_played == 2)
				break;
		}
		else
		{
			played.insert(*i);
		}
	}
	
	if (num_played == 2 && played.size() != current_track_list.size() - 1)
	{
		LOG_AUDIO << "Played twice with only " 
				  << lexical_cast<std::string>(played.size())
				  << " tracks between\n";
				  
		return false;
	}
	
	i = played_before.rbegin();
	if (i != played_before.rend())
	{
		i++;
		if (i != played_before.rend())
		{
			if (*i == name)
			{
				LOG_AUDIO << "Played just before previous\n";
				return false;
			}
		}
	}
	
	return true;
}

static const music_track &choose_track()
{
	assert(!current_track_list.empty());
	
	bool all_invalid = true;
	foreach(const music_track& mt, current_track_list)
	{
		if (!mt.name.empty())
		{
			all_invalid = false;
			break;
		}
	}
	if (all_invalid)
	{
		throw no_valid_tracks();
	}
	
	std::string name;
	unsigned int track = 0;
	
	if (current_track_list.size() > 1)
	{
		do
		{
			track = rand() % current_track_list.size();
		}
		while (!track_ok(current_track_list[track].name));
	}
	
	played_before.push_back(current_track_list[track].name);
	return current_track_list[track];
}

static std::string pick_one(const std::string &files)
{
	std::vector<std::string> names = utils::split(files);
	
	if (names.size() == 0)
		return "";
	if (names.size() == 1)
		return names[0];
		
#ifdef LOW_MEM
	return names[0];
#endif
	
	static std::map<std::string, unsigned int> prev_choices;
	unsigned int choice;
	
	if (prev_choices.find(files) != prev_choices.end())
	{
		choice = rand % (names.size() - 1);
		if (choice >= prev_choices[files])
			choice++;
		prev_choices[files] = choice;
	}
	else
	{
		choice = rand() % names.size();
		prev_choices.insert(std::pair<std::string, unsigned int>(files, choice));
	}
	
	return names[choice];
}

namespace 
{
struct audio_lock
{
	audio_lock()
	{
		SDL_LockAudio();
	}
	
	~audio_lock()
	{
		SDL_UnlockAudio();
	}
};
}

namespace sound
{
static void channel_finished_hook(int channel)
{
}

bool init_sound()
{
	LOG_AUDIO << "Initializing audio...\n";
	if (gSound == NULL)
		gSound = new iSound();
	
	if (!mix_ok)
	{
		mix_ok = true;
		set_sound_volume(preferences::sound_volume());
		set_UI_volume(preferences::UI_volume());
		set_music_volume(preferences::music_volume());
		set_bell_volume(preferences::bell_volume());
		
		LOG_AUDIO << "Audio initialized.\n";
		
		play_music();
	}
	return true;
}

void close_sound()
{
	if (mix_ok)
	{
		stop_bell();
		stop_UI_sound();
		stop_sound();
		stop_music();
		mix_ok = false;
	}
	
	delete gSound;
	gSound = nil;
	
	LOG_AUDIO << "Audio device released.\n";
}

void reset_sound()
{
	bool music = preferences::music_on();
	bool sound = preferences::sound_on();
	bool UI_sound = preferences::UI_sound_on();
	bool bell = preferences::turn_bell();
	
	if (music || sound || bell || UI_sound)
	{
		if (!sound::init_sound())
		{
		}
		if (!music)
			sound::stop_music();
		if (!sound)
			sound::stop_sound();
		if (!UI_sound)
			sound::stop_UI_sound();
		if (!bell)
			sound::stop_bell();	
	}
}

void stop_music()
{
	if (mix_ok)
	{
		gSound->stopMusic();
	}
}

void stop_sound()
{
	if (mix_ok)
	{
		gSound->stopSound(SOUND_SOURCES);
		gSound->stopSound(SOUND_FX);
	}
}

void stop_bell()
{
	if (mix_ok)
	{
		gSound->stopSound(SOUND_BELL);
		gSound->stopSound(SOUND_TIMER);
	}
}

void stop_UI_sound()
{
	if (mix_ok)
	{
		gSound->stopSound(SOUND_UI);
	}
}

void play_music_once(const std::string& file)
{
	current_track_list = std::vector<music_track>();
	current_track = music_track(file);
	play_music();
}

void play_no_music()
{
	current_track_list = std::vector<music_track>();
	current_track = music_track("");
	
	if (preferences::music_on() && mix_ok
		&& gSound->isMusicPlaying())
	{
		gSound->stopMusic();
	}
}

void empty_playlist()
{
	current_track_list = std::vector<music_track>();
}

void play_music()
{
	music_start_time = 1;
	want_new_music = true;
	no_fading = false;
	fadingout_time = current_track.ms_after;
}

static void play_new_music()
{
	music_start_time = 0;
	want_new_music = true;
	
	if (!preferences::music_on()  || !mix_ok || current_track.name.empty())
		return;
		
	const std::string& filename = get_binary_file_location("music", current_track.name);
	
	if (filename.empty())
	{
		const std::string bad_track	= current_track.name;
		ERR_AUDIO << "Could not open track '" << current_track.name << "', disabling for this playlist\n";
		
		current_track.name = "";
		foreach(music_track& trk, current_track_list)
		{
			if (trk.name == bad_track)
			{
				trk.name = "";
			}
		}
		return;
	}
	
	last_track = current_track;
	
	LOG_AUDIO << "Playing track '" << current_track.name << "'\n";
	int fading_time = current_track.ms_before;
	if (no_fading)
	{
		fading_time = 0;
	}
	
	gSound->playMusic(filename, 1);
	
	want_new_music = false;
}

void play_music_repeatedly(const std::string& name)
{
	if (name.empty())
		return;
		
	current_track_list = std::vector<music_track>(1, music_track(name));
	
	if (current_track.name != name)
	{
		current_track = music_track(name);
		play_music();
	}
}

void play_music_config(const config& music)
{
	struct music_track track(music["name"], music["ms_before"], music["ms_after"]);
	
	if (utils::string_bool(music["play_once"]))
	{
		current_track = track;
		current_track.once = true;
		play_music();
		return;
	}
	
	if (!utils::string_bool(music["append"]))
	{
		current_track_list = std::vector<music_track>();
	}
	
	std::vector<music_track>::const_iterator itor = current_track_list.begin();
	while(itor != current_track_list.end())
	{
		if (itor->name == track.name)
			break;
		++itor;
	}
	
	if (itor == current_track_list.end())
	{
		current_track_list.push_back(track);
	}
	else
	{
		ERR_AUDIO << "Tried to add duplicate track '" << track.name << "'\n";
	}
	
	if (utils::string_bool(music["immediate"]))
	{
		current_track = track;
		play_music();
	}
}

void music_thinker::process(events::pump_info& info)
{
	if (preferences::music_on())
	{
		try
		{
			if (!music_start_time && !current_track_list.empty()
				&& !gSound->isMusicPlaying())
			{
				current_track = choose_track();
				music_start_time = info.ticks();
				no_fading = true;
				fadingout_time = 0;
			}
			
			if (music_start_time && info.ticks(&music_refresh, music_refresh_rate) >= music_start_time - fadingout_time)
			{
				want_new_music = true;
			}
			
			if (want_new_music)
			{
				if (gSound->isMusicPlaying())
				{
					gSound->stopMusic();
				}
				play_new_music();
			}
		}
		catch(no_valid_tracks const&)
		{
			current_track_list.clear();
		}
	}
}

void commit_music_changes()
{
	std::vector<music_track>::iterator i;
	
	played_before = std::vector<std::string>();
	
	if (current_track.once)
		return;
	
	for (i = current_track_list.begin(); i != current_track_list.end(); i++)
	{
		if (current_track.name == i->name)
			return;
	}
	
	if (current_track_list.empty())
		return;
		
	current_track = choose_track();
	play_music();
}

void write_music_play_list(config& snapshot)
{
	std::vector<music_track>::iterator i;
	bool append = false;
	
	for (i = current_track_list.begin(); i != current_track_list.end(); i++)
	{
		i->write(snapshot, append);
		append = true;
	}
}

void reposition_sound(int id, unsigned int distance)
{
	for (unsigned int ch = 0; ch < channel_ids.size(); ++ch)
	{
		int& ch_id = channel_ids[ch];
		if (ch_id == id)
		{
			if (distance >= DISTANCE_SILENT)
			{
				gSound->stopSound(ch);
			}
			else
			{
			}
		}
	}
}

bool is_sound_playing(int id)
{
	return std::find(channel_ids.begin(), channel_ids.end(), id) != channel_ids.end();
}

void stop_sound(int id)
{
	reposition_sound(id, DISTANCE_SILENT);
}

void play_sound_positioned(const std::string& files, int id, int repeats, unsigned int distance)
{
	if (preferences::sound_on())
	{
		play_sound_internal(files, SOUND_SOURCES, repeats, distance, id);
	}
}

#if !defined(__IPHONEOS__)
struct chunk_load_exception {};

static Mix_Chunk* load_chunk(const std::string& file, channel_group group)
{
	sound_cache_iterator it_bgn, it_end;
	sound_cache_iterator it;
	
	sound_cache_chunk temp_chunk(file);
	it_bgn = sound_cache.begin(), it_end = sound_cache.end();
	it = std::find(it_bgn, it_end, temp_chunk);
	
	if (it != it_end)
	{
		if (it->group != group)
		{
			it->group = NULL_CHANNEL;
		}
		
		sound_cache.splice(it_bgn, sound_cache, it);
	}
	else
	{
		bool cache_full = (sound_cache.size() == max_cached_chunks);
		while(cache_full && it != it_bgn)
		{
			std::vector<MixChunk*>::iterator ch_end = channel_chunks.end();
			if (std::find(channel_chunks.begin(), ch_end, (--it)->get_data()) == ch_end)
			{
				sound_cache.erase(it);
				cache_full = false;
			}
		}
		if (cache_full)
		{
			LOG_AUDIO << "Maximum sound cache size reached and all are busy, skipping.\n";
			throw chunk_load_exception();
		}
		temp_chunk.group = group;
		std::string const &filename = get_binary_file_location("sounds", file);
		
		if (!filename.empty())
		{
			temp_chunk.set_data(Mix_LoadWAV(filename.c_str()));
		}
		else
		{
			ERR_AUDIO << "Could not load sound with empty filename\n";
			throw chunk_load_exception();
		}
		
		sound_cache.push_front(temp_chunk);
	}
	
	return sound_cache.begin()->get_data();
}
#endif

void play_sound_internal(const std::string& files, channel_group group, unsigned int repeats, 
	unsigned int distance, int id, int loop_ticks, int fadein_ticks)
{
	if (files.empty() || distance >= DISTANCE_SILENT || !mix_ok)
	{
		return;
	}
#ifdef __IPHONEOS__
	std::string file = pick_one(files);
	std::string const &filename = get_binary_file_location("sounds", file);
	gSound->playSound(filename, group);
#else
	audio_lock lock;
	
	int channel = Mix_GroupAvailable(group);
	int channel = group;
	if (channel == -1)
	{
		LOG_AUDIO << "All channels dedicated to group group(" << group << ") are busy, skipping.\n";
		return;
	}
	
	Mix_Chunk* chunk;
	std::string file = pick_one(files);
	
	try
	{
		chunk = load_chunk(file, group);
		assert(chunk);
	}
	catch(const chunk_load_exception&)
	{
		return;
	}
	
	if (group != SOUND_UI)
	{
		Mix_SetDistance(channel, distance);
	}
	
	int res;
	if (loop_ticks > 0)
	{
		if (fadein_ticks > 0)
		{
			res = Mix_FadeInChannelTimed(channel, chunk, -1, fadein_ticks, loop_ticks);
		}
		else
		{
			res = Mix_PlayChannel(channel, chunk, -1);
		}
		
		if (res >= 0)
		{
			Mix_ExpireChannel(channel, loop_ticks);
		}
	}
	else
	{
		if (fadein_ticks > 0)
		{
			res = Mix_FadeInChannel(channel, chunk, repeats, fadein_ticks);
		}
		else
		{
			res = Mix_PlayChannel(channel, chunk, repeats);
		}
	}
	
	if (res < 0)
	{
		ERR_AUDIO << "error playing sound effect: " << Mix_GetError() << "\n";
		return;
	}
	
	channel_ids[channel] = id;
	
	channel_chunks[res] = chunk;
#endif
}

void play_sound(const std::string& files, channel_group group, unsigned int repeats)
{
	if (preferences::sound_on())
	{
		play_sound_internal(files, SOUND_BELL);
	}
}

void play_bell(const std::string& files)
{
	if (preferences::sound_on() && preferences::turn_bell())
	{
		play_sound_internal(files, SOUND_BELL);
	}
}

void play_timer(const std::string& files, int loop_ticks, int fadein_ticks)
{
	if (preferences::sound_on())
	{
		play_sound_internal(files, SOUND_TIMER, 0, 0, -1, loop_ticks, fadein_ticks);
	}
}

void play_UI_sound(const std::string& files)
{
	if (preferences::UI_sound_on())
	{
		play_sound_internal(files, SOUND_UI);
	}
}

void set_music_volume(int vol)
{
	if (mix_ok && vol >= 0)
	{
		if (vol > MIX_MAX_VOLUME)
			vol = MIX_MAX_VOLUME;
			
		gSound->setMusicVolume(vol);
	}
}

void set_sound_volume(int vol)
{
	if (mix_ok && vol >= 0)
	{
		if (vol > MIX_MAX_VOLUME)
			vol = MIX_MAX_VOLUME;
			
		gSound->setVolume(SOUND_SOURCES, vol);
		gSound->setVolume(SOUND_FX, vol);
	}
}

void set_bell_volume(int vol)
{
	if (mix_ok && vol >= 0)
	{
		if (vol > MIX_MAX_VOLUME)
			vol = MIX_MAX_VOLUME;
			
		gSound->setVolume(SOUND_BELL, vol);
		gSound->setVolume(SOUND_TIMER, vol);
	}
}

void set_UI_volume(int vol)
{
	if (mix_ok && vol >= 0)
	{
		if (vol > MIX_MAX_VOLUME)
			vol = MIX_MAX_VOLUME;
		
		gSound->setVolume(SOUND_UI, vol);00
	}
}
}
